![]() |
![]() |
|
Vergessen Sie nicht, den Namespace CircleApplication, in dem die Klasse Circle definiert ist, mit using anzugeben. 4.3.2 Methoden mit Parameterliste
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public double GetFlaeche(double radius) { |
| double flaeche = 3.14 * Math.Pow(radius, 2); |
| return flaeche; |
| } |
Die Deklaration eines Parameters in der Methodendefinition erinnert an die Deklaration einer Variablen: Zuerst wird der Typ angegeben, danach folgt der Bezeichner. GetFlaeche definiert also einen Parameter namens radius vom Typ double. Zu beachten ist die Sichtbarkeit des Parameters: Er ist nur innerhalb der Methode bekannt. Damit ist seine Lebensdauer auf die Ausführungszeit der Methode beschränkt.
Nun wollen wir auch die parametrisierte Methode GetFlaeche testen:
| Circle meinKreis = new Circle(); |
| double kreisradius = 4.5; |
| double area = meinKreis.GetFlaeche(kreisradius); |
GetFlaeche setzt ein konkretes Objekt voraus. Deshalb muss zuerst die Klasse Circle instanziiert werden. Danach wird auf die Objektreferenz meinKreis die Methode GetFlaeche aufgerufen, wobei in den runden Klammern das Argument übergeben wird, das hier die Variable kreisradius mit dem Inhalt 4,5 ist. Die Methode nimmt das Argument im Parameter radius entgegen, der nun seinerseits ebenfalls den Inhalt 4,5 hat.
In einer Parameterliste können beliebig viele Parameter definiert werden. Das folgende Codefragment beschreibt die Klasse Rectangle mit der Methode GetRectFlaeche. Die Parameterliste dieser Methode definiert die beiden Parameter laenge und breite, die durch ein Komma voneinander getrennt werden. Aus den an diese Parameter übergebenen Werten wird die Grundfläche des Rechtecks berechnet und als Resultat des Methodenaufrufs geliefert:
| class Rectangle { |
| public double GetRectFlaeche(double laenge, double breite) { |
| double flaeche = laenge * breite; |
| return flaeche; |
| } |
| } |
Der Aufruf der Methode könnte folgendermaßen lauten:
| Rectangle rechteck = new Rectangle(); |
| double flaeche = rechteck.GetRectFlaeche(3, 6); |
Anstelle eines Literals können Sie auch Variablennamen angeben, die zur Laufzeit durch die entsprechenden Werte ersetzt werden:
| Rectangle rechteck = new Rectangle(); |
| double x = 3.4; |
| double y = 6.19; |
| double flaeche = rechteck.GetRectFlaeche(x, y); |
Bei Methoden, die mehr als einen Parameter erwarten, müssen Sie immer die Reihenfolge der übergebenen Argumente beachten: Das erste Argument wird dem ersten Parameter zugewiesen, das zweite Argument dem zweiten Parameter usw.
Methoden mit Rückgabewert sind der einfachste Weg, um zwischen einer Methode und ihrem Aufrufer Daten auszutauschen. Dabei wird ein Methodenaufruf wie ein Variablenname bewertet, da die Methode einen bestimmten Wert repräsentiert. So wäre es möglich, einen Methodenaufruf in einem Ausdruck als Operand einzusetzen, wie das folgende Beispiel zeigt, das die Methode GetFlaeche der Circle-Klasse dazu benutzt, um das Volumen eines Zylinders zu berechnen:
| Circle kreis = new Circle(); |
| double hoehe = 3; |
| double volumen = kreis.GetFlaeche(10.7) * hoehe; |
Einer Methode mit Rückgabewert muss gesagt werden, um welchen es sich handelt. Dazu dient das Schlüsselwort return, hinter dem der Rückgabewert angegeben wird. Der folgende Code zeigt noch einmal die Implementierung der parametrisierten Methode GetFlaeche in der Klasse Circle:
| public double GetFlaeche(double radius) { |
| double flaeche = 3.14 * Math.Pow(radius, 2); |
| return flaeche; |
| } |
Es ist nicht unbedingt notwendig, zuerst das Ergebnis einer Operation einer lokalen Variablen zuzuweisen. Die Operation darf auch direkt hinter return erfolgen:
| public double GetFlaeche(double radius) { |
| return 3.14 * Math.Pow(radius, 2); |
| } |
Der Typ des hinter return angegebenen Werts muss mit der Typangabe in der Methodensignatur übereinstimmen oder implizit in diesen konvertiert werden können. Andernfalls ist im return-Statement eine explizite Konvertierung erforderlich. Im folgenden Codefragment ist der Rückgabewert der Methode MyMethod per Definition vom Typ int. Weil der hinter return angegebene Ausdruck aber vom Typ double ist (die Methode Pow liefert per Definition double zurück) und implizit nicht in int konvertiert werden kann, ist eine explizite Konvertierung erforderlich:
| public int MyMethod(int intVal) { |
| return (int)Math.Pow(intVal, 4); |
| } |
Sobald das return-Statement erreicht wird, kehrt die Programmausführung zum aufrufenden Code zurück. Alle Anweisungen, die einem return folgen, werden nicht mehr ausgeführt.
Ist eine Methode nicht void, muss im Anweisungsblock in jedem Fall return enthalten und erreichbar sein, da der C#-Compiler die Kompilierung ansonsten mit einer Fehlermeldung abbricht. Daher ist auch die folgende Methodenimplementierung unzulässig, da keine der Bedingungen zur Ausführung der return-Anweisung führt, wenn dem Parameter die Zahl 3 übergeben wird. Der C#-Compiler ist intelligent genug, um das zu erkennen.
| public string TheAnswer(int x) { |
| if (x < 3) |
| return "Variable ist kleiner als 3"; |
| else if(x > 3) |
| return "Variable ist größer als 3"; |
| } |
| Hinweis Die return-Anweisung ist nicht nur auf Methoden mit Rückgabewert beschränkt. Auch Methoden ohne Rückgabewert (void) können damit vorzeitig verlassen werden. Bei void-Methoden ist die Angabe von return jedoch optional. |
Der Rückgabewert einer Methode muss nicht unbedingt vom Aufrufer entgegengenommen werden – er kann ihn auch ignorieren. Daher ist der folgende Aufruf der Methode GetFlaeche absolut zulässig, wenn auch in diesem Fall total unsinnig:
| kreis.GetFlaeche(); |
Wir wollen uns nun ein Beispiel ansehen, in dem der Rückgabewert einer Methode ein Array ist.
| // -------------------------------------------------------------- |
| // Beispiel: ...Kapitel 4\ArrayRueckgabe |
| // -------------------------------------------------------------- |
| namespace ArrayRueckgabe { |
| class Program { |
| static void Main(string[] args) { |
| ClassA myObj = new ClassA(); |
| // die Methode 'MyFunc' aufrufen und den Rückgabewert |
| // einem Array zuweisen |
| int[] arr = myObj.MyFunc(); |
| for(int i = 0; i < arr.Length; i++) |
| Console.WriteLine("arr[{0}] = {1}", i, arr[i]); |
| Console.ReadLine(); |
| } |
| } |
| // Klassendefinition |
| class ClassA { |
| // Instanzmethode |
| public int[] MyFunc() { |
| int[] intArr = {2, 4, 6, 8}; |
| return intArr; |
| } |
| } |
| } |
Soll eine Methode ein Array an den Aufrufer zurückliefern, muss dies in der Methodensignatur bekannt gegeben werden. In MyFunc wird das lokale Array intArr mit Daten gefüllt und dient als Argument der return-Anweisung:
| return intArr; |
Das ist vollkommen ausreichend, denn ein Array ist auch ein Objekt und der Name eines Arrays die Referenz auf den vom Array reservierten Speicherbereich. Der Aufrufer nimmt die Rückgabe mit
| int[] arr = myObj.MyFunc(); |
im Array arr in Empfang. Das Array arr ist damit initialisiert. Anschließend kann in üblicher Weise über die Indizes auf die einzelnen Elemente des Arrays zugegriffen werden.
Viele Methoden enthalten einen eigenen Satz von Variablen. Variablen, die innerhalb des Anweisungsblocks einer Methode deklariert sind, gelten als lokale Variablen.
| public void MyMethod() { |
| long lngVariable = 34; |
| // Anweisungen |
| } |
Lokale Variablen sind nur in der Methode sichtbar, in der sie deklariert sind. Programmcode, der sich außerhalb der Methode befindet, kann lokale Variablen weder sehen noch manipulieren oder auswerten. Das gilt auch für Aufrufverkettungen, wenn beispielsweise aus der Methode heraus eine zweite und aus dieser heraus wieder eine dritte Methode aufgerufen wird.
Die Lebensdauer einer lokalen Variablen ist auf die Ausführungszeit der Methode beschränkt. Sobald die Methodenausführung beendet wird, wird auch jede in ihr deklarierte lokale Variable aufgegeben, und der Inhalt geht verloren. Ein wiederholter Methodenaufruf hat zur Folge, dass eine lokale Variable neu erzeugt wird.
Eine lokal deklarierte Variable wird nicht automatisch mit einem typspezifischen Standardwert initialisiert. Sie sollten alle lokalen Variablen möglichst sofort initialisieren, ihnen also unter Berücksichtigung des Datentyps einen gültigen Startwert zuweisen, weil ein Zugriff auf eine nicht initialisierte Variable eine Fehlermeldung verursacht.
| public void MyMethod() { |
| int intVar; |
| // die folgende Anweisung verursacht einen Compilerfehler, |
| // weil intVar nicht initialisiert ist |
| Console.WriteLine(intVar); |
| } |
Der Begriff »lokale Variable« lässt sich noch weiter ausdehnen, da nicht jede Variable, die innerhalb einer Methode deklariert ist, auch eine Sichtbarkeit aufweist, die sich über den gesamten Anweisungsblock der Methode erstreckt. Sehen Sie sich dazu die Klasse MyClass an.
| class MyClass { |
| int iClassVariable; |
| public void SomeVariables() { |
| int intVar = 0; |
| // Anweisungen |
| if(intVar > 0) { |
| int intX = 1; |
| // Anweisungen |
| for(int i = 0; i <=100; i++) { |
| double dblVar = 3.14; |
| // Anweisungen |
| } |
| } |
| } |
| } |
Außerhalb jeglicher Methodendefinition befindet sich die Variable iClassVariable. Eine Variable, die auf Klassen- und nicht auf Methodenebene deklariert ist, wird auch als Feld bezeichnet. Grundsätzlich sind Felder in jeder Methode der Klasse sichtbar.
In SomeVariables sind einige Anweisungsblöcke ineinander verschachtelt. Anweisungsblöcke dienen nicht nur dazu, Anweisungssequenzen zusammenzufassen, sondern beschreiben gleichzeitig auch die Sichtbarkeit lokaler Variablen. Dabei wird die Sichtbarkeit von dem am nächsten stehenden, äußeren geschweiften Klammerpaar begrenzt. Deshalb beschränkt sich die Sichtbarkeit von dblVar auf den Anweisungsblock der for-Schleife und intX auf den Anweisungsblock des if-Statements. intX ist auch innerhalb der for-Schleife bekannt und kann dort sowohl ausgewertet als auch manipuliert werden. Die lokale Variable intVar ist uneingeschränkt in der gesamten Methode SomeVariables bekannt.
In C# dürfen Felder und lokale Variablen gleichnamig sein, wie das folgende Codefragment zeigt:
| class ClassA { |
| public int myValue; |
| public void Method1() { |
| int myValue = 0; |
| ... |
| myValue = 4711; |
| } |
| public void Method2() { |
| myValue = 25; |
| } |
| } |
ClassA enthält das Feld myValue, derselbe Bezeichner wurde in der Methode Method1 für eine lokale Variable gewählt. Eine Anweisung in Method1 wie beispielsweise
| myValue = 4711; |
verändert den Inhalt der Variablen, deren Gültigkeitsbereich der der Anweisung am nächsten stehende ist – in diesem Fall wird also der Inhalt der lokalen Variablen geändert und nicht das gleichnamige Feld. Soll in Method1 aber das gleichnamige, auf Klassenebene deklarierte Feld angesprochen werden, muss dem Feldnamen das Schlüsselwort this vorausgehen, z.B.:
| this.myValue = 245; |
Method2 manipuliert ebenfalls myValue. Da in Method2 die lokale Variable myValue der Methode Method1 unbekannt ist, wird der Wert direkt dem Feld zugewiesen. Es wäre aber nicht falsch, trotzdem this zu verwenden.
| this ist ein Schlüsselwort, das auf die aktuelle Instanz verweist, und bietet sich an, um auf Methoden oder Eigenschaften des aktuellen Objekts zuzugreifen. |
Zugriffsmodifizierer beschreiben die Sichtbarkeit eines Elements. Wie Sie bereits gelernt haben, kann eine Klasse nur public oder internal sein. In gleicher Weise werden aber auch die Sichtbarkeit und damit der Zugriff auf die Mitglieder einer Klasse festgeschrieben, zu denen unter anderem auch die Ihnen schon bekannten Felder und Methoden gehören. Den beiden bekannten Modifizierern gesellen sich noch weitere hinzu, die Sie der folgenden Tabelle entnehmen können.
| Zugriffsmodifizierer | Beschreibung |
| public | Der Zugriff unterliegt keinerlei Einschränkungen. |
| private | Der Zugriff auf ein private definiertes Mitglied ist nur innerhalb der Klasse möglich, in der das private Member definiert ist. Alle anderen Klassen sehen private Member nicht. Deshalb ist darauf auch kein Zugriff möglich. |
| protected | Der Zugriff auf protected Member ähnelt dem auf private definierte. Die Sichtbarkeit ist in gleicher Weise eingeschränkt, jedoch werden protected Mitglieder an abgeleitete Klassen vererbt. |
| internal | Die Zugriff auf internal Member ist nur aus den Klassen heraus gestattet, die sich in derselben Assembly befinden. |
| protected internal | Stellt eine Kombination aus den beiden Modifizierern protected und internal dar. |
Sowohl protected als auch die Kombination protected internal spielen in diesem Kapitel noch keine Rolle, weil sie in direktem Zusammenhang mit dem Vererbungskonzept stehen, dem wir uns erst später zuwenden.
| Die Angabe eines Zugriffsmodifizierers ist optional. Wird darauf verzichtet, gilt das Klassenmitglied als private deklariert. |
Nehmen wir an, wir hätten die Methode MyMethod in der Klasse ClassA wie folgt definiert:
| class ClassA { |
| public void MyMethod(int x, float y) { |
| // Anweisungen |
| } |
| } |
Wir könnten nun auf die Idee kommen, die Methode unter Übergabe von Literalen aufzurufen, also beispielsweise mit:
| ClassA myObj = new ClassA(); |
| myObj.MyMethod(7, 3.12); |
Der C#-Compiler wird diesen Versuch, denn bei einem solchen wird es bleiben, ablehnen, denn die Übergabe des zweiten Arguments ist falsch. Im ersten Moment mag das unverständlich sein, bei einer genaueren Analyse wird es aber verständlich, da die Übergabe eines Arguments an einen Parameter nichts anderes als eine Zuweisungsoperation ist:
| float y = 3.12 |
Ein Literal vom Typ einer Fließkommazahl wird von der Laufzeitumgebung grundsätzlich als double angesehen. Jetzt kommen die Richtlinien der impliziten Konvertierung ins Spiel, nach denen ein double implizit nicht in einen float konvertiert werden kann. Das Literal muss daher zuerst explizit in einen float konvertiert werden:
| obj.MyMethod(7, (float)3.12); |
Eine andere Alternative ist es, in der aufrufenden Methode eine Variable vom Typ float zu deklarieren, ihr den Wert 3,12 zu übergeben und dann die Variable selbst als Argument anzugeben:
| float fltVar = 3.12F; |
| obj.MyMethod(7, fltVar); |
Beachten Sie, hier das Typsuffix F bzw. f bei der Zuweisung des Dezimalzahl-Literals an die float-Variable anzugeben.
Das nächste Beispiel ist ein wenig komplexer. Bisher haben wir jeweils nur einfache Daten als Argument übergeben, nun sollen es mehrere typgleiche sein. Dazu benutzen wir einen Parameter vom Typ eines Arrays.
| // -------------------------------------------------------------- |
| // Beispiel: ...\Kapitel 4\ArrayUebergabe |
| // -------------------------------------------------------------- |
| namespace ArrayUebergabe { |
| class Program { |
| static void Main(string[] args) { |
| ClassA myObj = new ClassA(); |
| int[] myArr = {3, 6, 9, 4, 13, 22, 2, 29, 17}; |
| Console.Write("Der Maximalwert beträgt "); |
| Console.Write(myObj.GetMaxValue(myArr)); |
| Console.ReadLine(); |
| } |
| } |
| // Klassendefinition |
| class ClassA { |
| // Instanzmethode |
| public int GetMaxValue(int[] arr) { |
| int maxValue = arr[0]; |
| foreach(int element in arr) |
| if(element > maxValue) |
| maxValue = element; |
| return maxValue; |
| } |
| } |
| } |
Die Methode GetMaxValue hat die Aufgabe, aus dem im Parameter übergebenen Array den größten Wert zu ermitteln. Dazu wird in der Methode zuerst die int-Variable maxValue deklariert und ihr der Inhalt des 0-indizierten Array-Elements zugewiesen. In einer foreach-Schleife werden danach alle Array-Elemente durchlaufen und deren Inhalt geprüft. Ist dieser größer als der von maxValue, ersetzt der Array-Wert den alten Inhalt von maxValue. Am Ende wird maxValue an den Aufrufer zurückgegeben. Die foreach-Schleife bewirkt, dass das erste Array-Element insgesamt sogar zweimal ausgewertet wird: bei der Zuweisung an maxValue und in der Schleife. Wenn Sie das vermeiden wollen, können Sie auch eine einfache for-Schleife codieren:
| for(int index = 1; index < arr.Length; index++) {/*...*/} |
Der Parameter arr erwartet die Referenz auf ein Array. Da die Angabe des Array-Namens dieser Forderung entspricht, reicht die Übergabe von myArr beim Aufruf der Methode aus:
| myObj.GetMaxValue(myArr); |
Sehen Sie sich das folgende Beispiel an:
| // -------------------------------------------------------------- |
| // Beispiel: ...\Kapitel 4\Wertuebergabe |
| // -------------------------------------------------------------- |
| namespace Wertuebergabe { |
| class Program { |
| static void Main(string[] args) { |
| ClassA myClassA = new ClassA(); |
| myClassA.Init(); |
| } |
| } |
| // Klassendefinition |
| class ClassA { |
| public void Init() { |
| int intVar = 3; |
| Console.WriteLine("intVar vorher = {0}", intVar); |
| TestProc(intVar); |
| Console.WriteLine("intVar nachher = {0}", intVar); |
| Console.ReadLine(); |
| } |
| public void TestProc(int intPara) { |
| intPara = 550; |
| Console.WriteLine("intPara in TestProc = {0}", intPara); |
| } |
| } |
| } |
Aus der Main-Prozedur heraus wird die Klasse ClassA instanziiert und auf das Objekt die Methode Init aufgerufen. In Init ist die lokale Variable intVar deklariert. Nach der ersten Ausgabe des Variableninhalts an der Konsole wird aus Init heraus die Methode TestProc ausgeführt, der als Argument die lokale Variable intVar übergeben wird, die im Parameter intPara entgegengenommen wird.
In der ersten Anweisung der Methode TestProc wird der Inhalt von intPara in 550 geändert und anschließend gleichfalls an der Konsole ausgegeben, danach wird die Kontrolle des Programmablaufs wieder an die aufrufende Methode Init zurückgegeben, die ein weiteres Mal den Inhalt der lokalen Variablen intVar anzeigt. Starten Sie das Programm, lautet die Ausgabe an der Konsole:
| intVar vorher = 3 |
| intPara in TestProc = 550 |
| intVar nachher = 3 |
Festzuhalten bleibt, dass sich der Inhalt der lokalen Variablen intVar auch nach dem Aufruf der Methode TestProc nicht verändert hat.
Um zu verstehen, was sich bei diesem Methodenaufruf abspielt, müssen wir einen Blick in den Teilbereich des Speichers werfen, in dem die Daten vorgehalten werden. Zunächst wird für die Variable intVar Speicher allokiert. Nehmen wir an, es sei die Speicheradresse 10042 (10042 = &intVar). In diese Speicherzelle (genau genommen sind es natürlich vier Byte, die ein int für sich beansprucht) wird die Zahl 3 geschrieben.
Ein Parameter unterscheidet sich nicht von einer lokalen Variablen. Genau das ist der entscheidende Punkt, denn folgerichtig ist ein Parameter ebenfalls ein Synonym für eine bestimmte Adresse im Speicher. Mit der Übergabe des Arguments intVar beim Methodenaufruf wird von TestProc zunächst Speicher für den Parameter intPara allokiert – angenommen die Adresse 10050 (= &intPara). Danach wird der Inhalt des Arguments intVar – also 3 – in die Speicherzelle 10050 kopiert.
Ändert TestProc den Inhalt von intPara, wird die Änderung in die Adresse 10050 geschrieben. Damit weisen die beiden in unserem Beispiel angenommenen Speicheradressen die folgenden Inhalte auf:
10042 = &intVar = 3 10050 = &intPara = 550
Nachdem der Programmablauf zu der aufrufenden Methode zurückgekehrt ist, wird der Inhalt der Variablen intVar, also der Inhalt der Speicheradresse 10042, an der Konsole ausgegeben: Es ist die Zahl 3. Diese Technik der Argumentübergabe wird als Wertübergabe (engl.: Call by Value) bezeichnet. In der Abbildung 4.10 ist der Prozess der beschriebenen Wertübergabe schematisch dargestellt.
| Die Wertübergabe ist der Standard unter C# und allen CLS-konformen .NET-Sprachen. |

Hier klicken, um das Bild zu vergrößern
Abbildung 4.10 Wertparameter (Call by Value)
Nehmen wir nun zwei minimale Änderungen vor. Zuerst wird der Methodenaufruf in Init wie folgt codiert:
| TestProc(ref intVar); |
Im zweiten Schritt ergänzen wir in ähnlicher Weise auch die Parameterliste von TestProc:
| public void TestProc(ref int intPara) {...} |
Starten Sie jetzt das Beispiel noch einmal, wird dies an der Eingabeaufforderung zu folgender Ausgabe führen:
| intVar vorher = 3 |
| intPara in TestProc = 550 |
| intVar nachher = 550 |
Die Ergänzung sowohl des Methodenaufrufs als auch der Parameterliste um das Schlüsselwort ref hat also bedeutende Konsequenzen für die lokale Variable intVar in der Methode Init – sie hat nach dem Methodenaufruf genau den Inhalt angenommen, der dem Parameter intPara zugewiesen worden ist. Wie ist das zu erklären?
Beim Aufruf von TestProc mit
| TestProc(ref intVar); |
wird nicht mehr der Inhalt der Variablen intVar übergeben, sondern deren Speicheradresse &intVar, also10042. Der empfangende Parameter intPara muss selbstverständlich wissen, was ihn erwartet – nämlich die Speicheradresse eines int –, und wird daher ebenfalls mit ref deklariert. Für intPara muss die Methode natürlich auch weiterhin Speicher allokieren – gehen wir auch in diesem Fall noch einmal von der Adresse 10050 aus.
intPara wird ein Zeiger auf die Adresse 10042 der Variablen intVar übergeben. Alle Aufrufe an intPara werden nun an die Adresse &intVar, also 10042, umgeleitet. Die Methode TestProc weist dem Parameter intPara die Zahl 550 zu, die nun in die Adresse 10042 geschrieben wird. Damit gilt:
intPara = intVar = 550
Nachdem der Programmablauf an die aufrufende Methode zurückgegeben worden ist, wird an der Konsole der Inhalt der Variablen intVar – also der Inhalt, der unter der Adresse 10042 zu finden ist – angezeigt: Es handelt sich um die Zahl 550. Diese Technik der Parameterübergabe wird als Referenzübergabe (engl.: Call by Reference) bezeichnet (siehe auch Abbildung 4.11).

Hier klicken, um das Bild zu vergrößern
Abbildung 4.11 Referenzparameter (Call by Reference)
Fassen wir in dieser Stelle kurz zusammen.
| Wir sprechen von einer Wertübergabe (Call by Value), wenn der Parameter einer Methode eine Kopie des Arguments erhält und sich Änderungen nur auf die Kopie, nicht aber auf das Original auswirken. Ein solcher Parameter wird auch als Wertparameter bezeichnet. |
| Geht der Definition eines Methodenparameters das Schlüsselwort ref voraus, erhält der Parameter einen Verweis auf die Speicheradresse des übergebenen Arguments. Änderungen werden in das Original geschrieben. Diese Parameter werden als ref- oder Referenzparameter bezeichnet, die Übergabe als Referenzübergabe (Call by Reference). |
| Während die Übergabe an einen Wertparameter keinen besonderen Regeln unterliegt, ist die Übergabe an einen Referenzparameter an mehrere Bedingungen geknüpft: |
| In der Parameterliste der Methode muss der Parameter mit dem Schlüsselwort ref gekennzeichnet werden. |
| Im Methodenaufruf muss dem zu übergebenden Argument das Schlüsselwort ref vorangestellt werden. |
| Das zu übergebende Argument muss initialisiert sein, d. h., es muss einen gültigen Wert aufweisen. |
| Das Übergabeargument darf keine Konstante sein. Lautet die Signatur einer Methode beispielsweise |
public void ProcA(ref int x)
| ist der folgende Methodenaufruf falsch: | |
obj.ProcA(ref 16);
| Das Übergabeargument darf nicht direkt aus einem berechneten Ausdruck in Form eines Methodenaufrufs bezogen werden, z.B.: |
obj.ProcA(ref a, ref obj.ProcB());
Zusätzlich zu den beiden erläuterten Übergabetechniken kann ein Parameter auch mit out spezifiziert werden, der in derselben Weise wie ref verwendet wird, nämlich sowohl als Modifizierer des Übergabearguments als auch als Modifizierer des empfangenen Parameters in der Methodendefinition. Obwohl der Effekt, der mit out erzielt werden kann, derselbe ist, den wir auch mit ref erreichen, gibt es zwischen ref und out zwei entscheidende Unterschiede:
| Während die Übergabe einer nicht initialisierten Variablen mit ref zu einem Kompilierfehler führt, ist dies bei out zulässig. |
| In der Methode muss einem out-Parameter ein Wert zugewiesen werden, während das bei einem ref-Parameter nicht unbedingt notwendig ist. |
In der folgenden Definition der Klasse ClassA nimmt die Methode ProcA einen out-Parameter entgegen und weist ihm einen Wert zu.
| class ClassA { |
| public void ProcA(out int x) { |
| x = 17; |
| } |
| } |
Diese Methode kann wie folgt aufgerufen werden:
| int intVar; // intVar ist nicht initialisiert |
| ClassA obj = new ClassA(); |
| obj.ProcA(out intVar); |
| Console.WriteLine(intVar); |
Die abschließende Konsolenausgabe lautet 17. Beachten Sie, dass beim Methodenaufruf ebenfalls das Schlüsselwort out angegeben wird, so wie es der entsprechende Parameter der aufgerufenen Methode verlangt.
Obwohl Sie mit out eine uninitialisierte Variable an eine Methode übergeben können, ist das keine unbedingte Vorschrift, denn die Variable dürfte durchaus schon initialisiert sein:
| int a = 2; |
| ClassA obj = new ClassA(); |
| obj.ProcA(out a); |
Allerdings müssen Sie dabei unbedingt einen wichtigen Punkt beachten: In der aufgerufenen Methode muss dem out-Parameter ein Wert zugewiesen werden – unabhängig davon, ob der Aufrufer eine initialisierte oder uninitialisierte Variable übergibt. Die Methode betrachtet einen out-Parameter grundsätzlich als nicht initialisiert und wird den Inhalt der Speicheradresse mit absoluter Sicherheit überschreiben. In der aufrufenden Methode hat das ziemlich brutale Folgen: Die Variable, die als Argument übergeben wird, hat nach dem Aufruf garantiert einen anderen Inhalt.
Die Definition eines Referenzparameters birgt gewisse Risiken, derer man sich bewusst sein sollte: Ein ref-Parameter kann den Originalwert manipulieren – mit möglicherweise falschen Ergebnissen des laufenden Programms, wenn dies unkontrolliert geschieht, ein out-Parameter wird das in jedem Fall tun. Richtig eingesetzt erhöhen Referenzparameter die Flexibilität der Programmierung. Nehmen wir noch einmal das Beispiel des vorhergehenden Abschnitts:
| public void TestProc(ref int intPara){/*...*/} |
Der Parameter intPara manipuliert die Speicheradresse des übergebenen Arguments. Man kann das auch anders ausdrücken: Ein Referenzparameter ermöglicht die Rückgabe eines berechneten Werts, allerdings nicht als Rückgabewert des Methodenaufrufs, sondern über die Parameterliste.
Soll ein Ergebnis an eine aufrufende Methode zurückgegeben werden, stehen damit zwei Alternativen zur Wahl:
| eine Methode mit Rückgabewert (mit return) |
| eine benutzerdefinierte Methode mit einem ref- oder out-Parameter |
Verdeutlichen wir uns das an einer Methode, die das Volumen einer Kugel als Ergebnis des Aufrufs liefert.
| public double VolumeBall(float Radius) { |
| return 4 * 3.14 * Math.Pow(Radius, 3) / 3; |
| } |
Der Aufruf von VolumeBall erfolgt unter der Übergabe des Radius. Der Kugelradius darf innerhalb der Methode nicht verändert werden, daher ist der Parameter als Wertparameter festgelegt. Der Aufruf der Methode VolumeBall erfolgt mit
| Class1 obj = new Class1(); |
| double volume = obj.VolumeBall(2); |
Das Resultat kann anschließend der Variablen volume entnommen werden.
Dieselbe Funktionalität soll nun mit einer void-Methode realisiert werden. Dazu werden in der Parameterliste zwei Parameter deklariert: Der erste nimmt den Radius der Kugel entgegen, der zweite dient zur Rückgabe des Ergebnisses und wird out deklariert. Die Reihenfolge der Parameter spielt keine Rolle und ist willkürlich festgelegt. Wir ziehen hier den Modifizierer out dem Modifizierer ref vor, weil wir im Parameter volumen keinen Übergabewert seitens des Aufrufers erwarten.
| public void VolumeBall(float Radius, out double volumen) { |
| volumen = 4 * 3.14 * Math.Pow(Radius, 3) / 3; |
| } |
In der aufrufenden Methode muss in jedem Fall noch eine zweite Variable deklariert werden – im folgenden Codefragment wird sie dblVolume genannt und der Methode VolumeBall als zweites Argument übergeben. Wie Sie bereits wissen, wird damit aufgrund der out-Deklaration des empfangenden Parameters die Speicheradresse der Variablen dblVolume bekannt gegeben, in welche die Methode VolumeBall das Ergebnis der Berechnung schreiben soll.
| Class1 obj = new Class1(); |
| double dblVolume; |
| obj.VolumeBall(2, out dblVolume); |
| Console.WriteLine(dblVolume); |
In diesem Codefragment wird dem Aufrufer nur ein Ergebnis geliefert, letztendlich ist die Anzahl aber praktisch unbegrenzt.
In allen bisherigen Ausführungen haben wir der Parameterliste nur einfache Datentypen wie int oder long übergeben. Wie Sie wissen, ordnet .NET alle Datentypen zwei Gruppen zu: entweder den Werte- oder den Referenztypen. Zu den Werttypen gehören beispielsweise bool, byte, int, double usw., zu den Referenztypen alle Typen, die auf einer Klassendefinition basieren.
| Hinweis Sie werden im nächsten Kapitel noch erfahren, dass Wertetypen nicht als Klassen, sondern als Strukturen definiert sind. |
Selbstverständlich können wir auch Parameter deklarieren, denen ein Referenztyp zugrunde liegt. Dazu ein Beispiel.
| class Program { |
| static void Main(string[] args) { |
| ClassA clsA = new ClassA(); |
| ClassB clsB = new ClassB(); |
| clsB.ChangeObject(clsA); |
| Console.WriteLine(clsA.TestValue); |
| Console.ReadLine(); |
| } |
| } |
| class ClassA { |
| public int TestValue = 500; |
| } |
| class ClassB { |
| public void ChangeObject(ClassA obj) { |
| obj.TestValue = 4711; |
| } |
| } |
Hier sind die beiden Klassen ClassA und ClassB definiert. ClassB hat eine Methode, der im Parameter obj ein Objekt vom Typ ClassA übergeben wird. In der Methode wird das Feld TestValue des ClassA-Objekts manipuliert.
In Main wird je ein Objekt der beiden Klassen erzeugt. Dem Aufruf der Methode ChangeObject des ClassB-Objekts wird das Objekt vom Typ ClassA übergeben. Nach dem Methodeaufruf wird an der Konsole der Inhalt des Feldes TestValue des ClassA-Objekts angezeigt – es ist der Wert 4711. Das mag im ersten Moment erstaunen, denn wir haben es in diesem Beispiel mit einem Wertparameter zu tun. Nach eingehender Überlegung wird man aber zu der Erkenntnis kommen, dass eine Objektvariable ein Verweis auf eine Speicheradresse ist, unter der das Objekt zu finden ist. Der Verweis wird in den Parameter kopiert und arbeitet dann konsequenterweise mit dem Originalobjekt.
Dasselbe Ergebnis erhält man, wenn man den Parameter als ref-Parameter deklariert – auch hierbei wird das Feld des Originalobjekts verändert.
Nun nehmen wir eine Ergänzung in der Methode der Klasse ClassB vor:
| class ClassB { |
| public void ChangeObject(ClassA obj) { |
| obj = new ClassA(); |
| obj.TestValue = 4711; |
| } |
| } |
obj wird beim Aufruf von ChangeObject der Verweis auf das Originalobjekt übergeben. In der Methode wird der Verweis jedoch »umgebogen«, indem ihm der Verweis auf ein neues ClassA-Objekt zugewiesen wird. In diesem Moment liegen zwei Objekte vom Typ ClassA vor. Der Aufrufer merkt von diesem Vorgang nichts. Er behält weiterhin die Referenz auf das Original, das sich nach Beendigung der Methode auch eindeutig durch das unveränderte Feld (500) zu erkennen gibt.
Eine Änderung des Parameters obj in der Weise, ihm das Schlüsselwort ref voranzustellen, hat allerdings Konsequenzen für den Aufrufer. Denn nun wird das Originalobjekt zerstört und durch das neue ersetzt. Das lässt sich sehr einfach nachweisen, weil an der Konsole der Inhalt von TestValue zu 4711 ausgegeben wird.
Zusammenfassend lässt sich feststellen, dass sich eine Wert- oder Referenzübergabe bei Referenztypen nur dann auswirkt, wenn in der aufgerufenen Methode der Parameter durch Zuweisung einer neuen Referenz überschrieben wird. Es gelten dabei dieselben Gesetze wie bei den Werttypen.
Stellen Sie sich vor, Sie beabsichtigen, eine Methode zu entwickeln, um Zahlen zu addieren. Eine Addition ist nur dann sinnvoll, wenn aus wenigstens zwei Zahlen eine Summe gebildet wird. Daher definieren Sie die Methode wie folgt:
| public long Addition(int value1, int value2) { |
| return value1 + value2; |
| } |
Vielleicht haben Sie danach noch die Idee, nicht nur zwei Zahlen, sondern drei bzw. vier zu addieren. Im folgenden Abschnitt werden Sie erfahren, dass C# die Methodenüberladung unterstützt. Methodenüberladung bedeutet, dass in einer Klasse mehrere gleichnamige Methoden definiert werden können, die sich nur in der Parameterliste unterscheiden. Demnach dürfen Sie auch die Methode Addition wie folgt überladen, um den Anforderungen zu genügen:
| public long Addition(int value1, int value2, int value3) |
| {/*...*/} |
| public long Addition(int value1, int value2, int value3, int value4) |
| {/*...*/} |
Wenn Ihnen dieser Ansatz kritiklos gefällt, sollten Sie sich mit der Frage auseinander setzen, wie viele überladene Methoden Sie maximal zu schreiben bereit sind, wenn möglicherweise nicht nur vier, sondern 10 oder 25 oder beliebig viele Zahlen addiert werden sollen.
Es muss für diese Problemstellung eine bessere Lösung geben – und es gibt sie auch: Sie definieren einen Parameter mit dem Modifizierer params. Bisher waren alle Parameter so definiert, dass jedem Parameter in der Parameterliste einer Methode genau ein Argument übergeben werden musste. Nun erhalten wir ein Mittel, um eine unbestimmte Anzahl von Parametern übergeben zu können, ohne eine Methode unüberschaubar oft überladen zu müssen.
Die Deklaration eines Parameters mit params erlaubt einer Methode, eine beliebige und im Voraus auch unbestimmte Anzahl von Übergabeargumenten zu akzeptieren. Die Übergabeargumente werden der Reihe nach in ein Array geschrieben.
Jetzt kann die Methode Addition den letzten Schliff erhalten. Da eine Addition voraussetzt, dass zumindest zwei Summanden an der Operation beteiligt sind, werden zuerst zwei Parameter definiert und anschließend ein params-Parameter für alle weiteren Übergabeargumente.
| public long Addition(int value1, int value2, params int[] liste) { |
| long summe = value1 + value2; |
| foreach(int z in liste) |
| summe += z; |
| return summe; |
| } |
Werden einem params-Parameter Argumente zugewiesen, wird das Array anhand der Anzahl der übergebenen Argumente implizit dimensioniert. In unserem Beispiel werden alle Elemente des Arrays in einer Schleife addiert und in der lokalen Variablen summe temporär zwischengespeichert. Nachdem auch für das letzte Element die Schleife durchlaufen ist, wird mit der return-Anweisung das Ergebnis an den Aufrufer übermittelt.
| Mit einem params-Parameter sind ein paar Regeln verbunden, die eingehalten werden müssen: In der Parameterliste darf nur ein Parameter mit params festgelegt werden. Ein params-Parameter ist immer das letzte Element einer Parameterliste. Eine Kombination mit den Modifikatoren out oder ref ist unzulässig. Ein params-Parameter ist immer eindimensional. |
Wenn Sie eine Methode aufrufen, die einen params-Parameter enthält, haben Sie zwei Möglichkeiten, diesem Werte zuzuweisen:
| Sie übergeben die Referenz auf ein Array, z.B.: |
int[] myList = {1,2,3};
Console.WriteLine(obj.Addition(15, 19, myList));
| Sie übergeben diesem Methodenparameter eine Liste von Elementen: |
obj.Addition(1, 2, 3, 4, 5, 6);
Vielleicht stellen Sie sich an dieser Stelle die Frage, ob nicht die einfache Deklaration als Array dieselbe Leistung erbringen würde? Mit anderen Worten: Wo liegt der Unterschied zwischen den beiden Methodensignaturen
| public long Addition(params int[] liste) {/*...*/} |
und
| public long Addition(int[] liste) {/*...*/} |
wenn beide die Übergabe eines Arrays ermöglichen?
Zwischen beiden gibt es einen wesentlichen Unterschied: Einem params-Parameter muss nicht zwangsläufig ein Wert oder Array übergeben werden, bei einem herkömmlichen Array ist das in jedem Fall Pflicht.
Methoden beschreiben das Verhalten eines Objekts, die Parameter legen die Randbedingungen des Methodenaufrufs fest. Gegen die Idee, grundsätzlich allen Methoden einer Klasse unterschiedliche Bezeichner zu geben, ist zunächst prinzipiell nichts einzuwenden. Es kann sich allerdings sehr schnell herausstellen, dass es sinnvoll ist, zwei Methoden gleich zu benennen. Denken Sie beispielsweise an die Methoden GetUmfang und GetFlaeche der weiter oben beschriebenen Klasse Circle:
| public class Circle { |
| public double Radius; |
| // Methoden |
| public double GetFlaeche() { |
| return 3.14 * Math.Pow(Radius, 2); |
| } |
| public double GetUmfang() { |
| return 2 * 3.14 * Radius; |
| } |
| } |
Beide Methoden sind daraufhin ausgelegt, auf das objektspezifische Feld Radius zuzugreifen und daraus sowohl den Umfang als auch die Grundfläche zu ermitteln.
Eine weitere denkbare Aufrufalternative könnte auch so aussehen, dass den beiden Methoden der Radius als Argument des Methodenaufrufs übergeben wird, ein Benutzer der Klasse also den folgenden Code schreibt:
| Circle meinKreis = new Circle(); |
| double area = meinKreis.GetFlaeche(12.5); |
| double circumference = meinKreis.GetUmfang(12.5); |
Mit der Implementierung der Klasse Circle ist das noch nicht möglich, denn dazu sind Methoden mit einer entsprechenden Parameterliste notwendig. Zweifelsfrei wäre es unvorteilhaft, die beiden neuen Methoden anders zu bezeichnen als die beiden schon existierenden, die im Grunde genommen die gleiche Funktionalität haben. Die Klasse würde unübersichtlich werden, und der Einarbeitungsaufwand für einen Entwickler, der die Klasse Circle benutzt, wäre größer.
Jetzt kommt die Technik der Methodenüberladung ins Spiel, die es erlaubt, mehrere gleichnamige Methoden, die sich in der Parameterliste unterscheiden, in einer Klasse zu definieren. In der .NET-Klassenbibliothek wird davon intensiv Gebrauch gemacht. Denken Sie nur an die schon häufiger benutzte Methode WriteLine der Klasse Console. Obwohl Sie dieser Methode Argumente unterschiedlichen Datentyps übergeben und sogar die Anzahl der Argumente variieren – Sie rufen immer die WriteLine- Methode auf und keine anders lautende.
Eine Methodenüberladung wird meist dann eingesetzt, um die gleiche Basisfunktionalität mit einer unterschiedlichen Codeimplementierung bereitzustellen.
| Von Methodenüberladung wird gesprochen, wenn sich gleichnamige Methoden in der Anzahl der Parameter der Parameterliste unterscheiden, bei gleicher Gesamtanzahl der Parameter der Typ zumindest eines Parameters anders festgelegt ist, sich zwei typgleiche Parameter darin unterscheiden, dass der erste als Wert- und der zweite als Referenzparameter mit out bzw. ref definiert ist. |
Gemäß den Regeln der Methodenüberladung gelten die folgenden Methodendefinitionen einer Klasse als überladen:
| public void MyMethod() {} |
| public void MyMethod(byte x) {} |
| public void MyMethod(long x) {} |
| public void MyMethod(long x, long y) {} |
| public void MyMethod(ref long x) {} |
Eine Methode gilt nicht als überladen, wenn
| sich die Parameter nur im Bezeichner unterscheiden, |
| sich die Parameter einzig und allein im Modifizierer ref und out unterscheiden, |
| die Rückgabewerte der Methoden verschiedenen Datentyps sind, |
| durch den Modifizierer static eine der beiden Methoden als statisch gekennzeichnet wird. (Anmerkung: Statische Methoden werden im Kapitel 5 behandelt.) |
Unter Einbeziehung der Regeln zur Methodenüberladung könnte die Klasse Circle nun wie folgt aussehen:
| public class Circle { |
| public double Radius; |
| // ------ Instanzmethoden ------ |
| public double GetFlaeche() { |
| return 3.14 * Math.Pow(Radius, 2); |
| } |
| public double GetFlaeche(double radius) { |
| return 3.14 * Math.Pow(radius, 2); |
| } |
| public double GetUmfang() { |
| return 2 * 3.14 * Radius; |
| } |
| public double GetUmfang(double radius) { |
| return 2 * 3.14 * radius; |
| } |
| } |
In der Klasse Circle sind nun die Methoden GetUmfang und GetFlaeche überladen: Jeweils eine Variante ist parameterlos, die andere erwartet als Übergabeargument den Radius des Kreises. Beachten Sie, dass die parameterlosen Methoden den Inhalt des Feldes Radius zur Berechnung heranziehen, während die parametrisierten dazu auf den Parameter radius zurückgreifen.
Im folgenden Codebeispiel soll auf eine mögliche Problematik im Zusammenhang mit der Methodenüberladung hingewiesen werden. Betrachten Sie dazu die Definition von Class1.
| class Class1 { |
| static void Main(string[] args) { |
| Class1 obj = new Class1(); |
| byte var = 25; |
| obj.MyMethod(var); |
| Console.ReadLine(); |
| } |
| public void MyMethod(int x) { |
| Console.WriteLine("Integer"); |
| } |
| public void MyMethod(long x) { |
| Console.WriteLine("Long"); |
| } |
| } |
Die Klasse definiert die Methode MyMethod, die überladen ist und mit der Übergabe eines int- bzw. long-Arguments aufgerufen werden kann. In der Main-Methode wird die Klasse instanziiert und die Methode MyMethod unter Übergabe eine Arguments vom Typ byte aufgerufen. Doch welche Überladung wird nun ausgeführt? Die, die ein Argument als int entgegennimmt, oder diejenige, die einen long-Parameter definiert? Unterscheiden sich Codeimplementierungen in beiden Methoden, kommt der richtigen Antwort auf diese Frage entscheidende Bedeutung zu.
Ein Testlauf des Programmcodes beweist: Es wird die Methode aufgerufen, die den Typ des Parameters als int deklariert. Beim Aufruf findet eine implizite Datentypkonvertierung in der Weise statt, dass die Methode aufgerufen wird, deren Parameter dem Typ des entgegengenommenen Arguments am nächsten kommt.
Soll der Aufruf an die Methode gehen, die einen long-Parameter definiert, muss der Typ des Übergabearguments konvertiert werden, also:
| obj.MyMethod((long)var); |
| Es wird zwischen Methoden mit Rückgabewert und Methoden ohne Rückgabewert unterschieden. Methoden ohne Rückgabewert müssen anstelle des Rückgabetyps das Schlüsselwort void enthalten. |
| Liefert eine Methode dem Aufrufer ein Resultat, wird dieses mit der Anweisung return bereitgestellt. Nach der Ausführung von return wird die Kontrolle des Programmablaufs sofort an den Aufrufer zurückgegeben. |
| Eine Methode ohne Rückgabewert kann mit return die Programmkontrolle an den Aufrufer zurückgeben. |
| Variablen, die innerhalb einer Methode deklariert werden, sind nur innerhalb der Methode bekannt. |
| Parameter sind entweder Wert- oder Referenzparameter mit out bzw. ref. Standard unter C# sind Wertparameter. |
| Ein ref-Parameter setzt voraus, dass das übergebene Argument initialisiert ist, ein out-Parameter kann auch ein nicht initialisiertes Argument ohne Fehlermeldung entgegennehmen. |
| Mit dem Modifizierer params kann der Aufrufer einer Methode eine beliebige Anzahl typgleicher Argumente übergeben. |
| In der Parameterliste einer Methode darf nur ein Parameter mit params festgelegt werden, der immer das letzte Element einer Parameterliste und eindimensional ist. |
| Ein params-Parameter ist ein Wertparameter. Eine Kombination mit den Modifikatoren out oder ref ist unzulässig. |
| Unter Methodenüberladung wird die Definitionen mehrerer gleichnamiger Methoden in einer Klasse verstanden. Die Methoden müssen sich in der Parameterliste unterscheiden. |
| << zurück |
|
||||||||||||
|
||||||||||||
| ||||||||||||